Download dependencies and run tensorboard in the background:

!pip install tensorflow lightfm pandas
%load_ext tensorboard
!tensorboard --logdir 2020-09-11-neural_collaborative_filter/logs &

Data

Interaction matrix:
[[5 3 4 3 3 5 4 0 5 3]
 [4 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [4 0 0 0 0 0 0 4 4 0]
 [0 0 0 5 0 0 5 5 5 4]
 [0 0 0 0 0 0 3 0 0 0]
 [0 0 0 0 0 0 4 0 0 0]
 [4 0 0 4 0 0 0 0 4 0]]

# collapse
for dataset in ["test", "train"]:
    data[dataset] = (data[dataset].toarray() > 0).astype("int8")

# Make the ratings binary
print("Interaction matrix:")
print(data["train"][:10, :10])

print("\nRatings:")
unique_ratings = np.unique(data["train"])
print(unique_ratings)
Interaction matrix:
[[1 1 1 1 1 1 1 0 1 1]
 [1 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [1 0 0 0 0 0 0 1 1 0]
 [0 0 0 1 0 0 1 1 1 1]
 [0 0 0 0 0 0 1 0 0 0]
 [0 0 0 0 0 0 1 0 0 0]
 [1 0 0 1 0 0 0 0 1 0]]

Ratings:
[0 1]
from typing import List


def wide_to_long(wide: np.array, possible_ratings: List[int]) -> np.array:
    """Go from wide table to long.
    :param wide: wide array with user-item interactions
    :param possible_ratings: list of possible ratings that we may have."""

    def _get_ratings(arr: np.array, rating: int) -> np.array:
        """Generate long array for the rating provided
        :param arr: wide array with user-item interactions
        :param rating: the rating that we are interested"""
        idx = np.where(arr == rating)
        return np.vstack(
            (idx[0], idx[1], np.ones(idx[0].size, dtype="int8") * rating)
        ).T

    long_arrays = []
    for r in possible_ratings:
        long_arrays.append(_get_ratings(wide, r))

    return np.vstack(long_arrays)
long_train = wide_to_long(data["train"], unique_ratings)
df_train = pd.DataFrame(long_train, columns=["user_id", "item_id", "interaction"])
All interactions:
user_id item_id interaction
0 0 7 0
1 0 10 0
2 0 19 0
3 0 20 0
4 0 26 0
Only positive interactions:
user_id item_id interaction
1511499 0 0 1
1511500 0 1 1
1511501 0 2 1
1511502 0 3 1
1511503 0 4 1

The model (Neural Collaborative Filtering)

import tensorflow.keras as keras
from tensorflow.keras.layers import (
    Concatenate,
    Dense,
    Embedding,
    Flatten,
    Input,
    Multiply,
)
from tensorflow.keras.models import Model
from tensorflow.keras.regularizers import l2


def create_ncf(
    number_of_users: int,
    number_of_items: int,
    latent_dim_mf: int = 4,
    latent_dim_mlp: int = 32,
    reg_mf: int = 0,
    reg_mlp: int = 0.01,
    dense_layers: List[int] = [8, 4],
    reg_layers: List[int] = [0.01, 0.01],
    activation_dense: str = "relu",
) -> keras.Model:

    # input layer
    user = Input(shape=(), dtype="int32", name="user_id")
    item = Input(shape=(), dtype="int32", name="item_id")

    # embedding layers
    mf_user_embedding = Embedding(
        input_dim=number_of_users,
        output_dim=latent_dim_mf,
        name="mf_user_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mf),
        input_length=1,
    )
    mf_item_embedding = Embedding(
        input_dim=number_of_items,
        output_dim=latent_dim_mf,
        name="mf_item_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mf),
        input_length=1,
    )

    mlp_user_embedding = Embedding(
        input_dim=number_of_users,
        output_dim=latent_dim_mlp,
        name="mlp_user_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mlp),
        input_length=1,
    )
    mlp_item_embedding = Embedding(
        input_dim=number_of_items,
        output_dim=latent_dim_mlp,
        name="mlp_item_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mlp),
        input_length=1,
    )

    # MF vector
    mf_user_latent = Flatten()(mf_user_embedding(user))
    mf_item_latent = Flatten()(mf_item_embedding(item))
    mf_cat_latent = Multiply()([mf_user_latent, mf_item_latent])

    # MLP vector
    mlp_user_latent = Flatten()(mlp_user_embedding(user))
    mlp_item_latent = Flatten()(mlp_item_embedding(item))
    mlp_cat_latent = Concatenate()([mlp_user_latent, mlp_item_latent])

    mlp_vector = mlp_cat_latent

    # build dense layers for model
    for i in range(len(dense_layers)):
        layer = Dense(
            dense_layers[i],
            activity_regularizer=l2(reg_layers[i]),
            activation=activation_dense,
            name="layer%d" % i,
        )
        mlp_vector = layer(mlp_vector)

    predict_layer = Concatenate()([mf_cat_latent, mlp_vector])

    result = Dense(
        1, activation="sigmoid", kernel_initializer="lecun_uniform", name="interaction"
    )

    output = result(predict_layer)

    model = Model(
        inputs=[user, item],
        outputs=[output],
    )

    return model

# collapse
from tensorflow.keras.optimizers import Adam

n_users, n_items = data["train"].shape
ncf_model = create_ncf(n_users, n_items)

ncf_model.compile(
    optimizer=Adam(),
    loss="binary_crossentropy",
    metrics=[
        tf.keras.metrics.TruePositives(name="tp"),
        tf.keras.metrics.FalsePositives(name="fp"),
        tf.keras.metrics.TrueNegatives(name="tn"),
        tf.keras.metrics.FalseNegatives(name="fn"),
        tf.keras.metrics.BinaryAccuracy(name="accuracy"),
        tf.keras.metrics.Precision(name="precision"),
        tf.keras.metrics.Recall(name="recall"),
        tf.keras.metrics.AUC(name="auc"),
    ],
)
ncf_model._name = "neural_collaborative_filtering"
ncf_model.summary()
Model: "neural_collaborative_filtering"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
user_id (InputLayer)            [(None,)]            0                                            
__________________________________________________________________________________________________
item_id (InputLayer)            [(None,)]            0                                            
__________________________________________________________________________________________________
mlp_user_embedding (Embedding)  (None, 32)           30176       user_id[0][0]                    
__________________________________________________________________________________________________
mlp_item_embedding (Embedding)  (None, 32)           53824       item_id[0][0]                    
__________________________________________________________________________________________________
flatten_2 (Flatten)             (None, 32)           0           mlp_user_embedding[0][0]         
__________________________________________________________________________________________________
flatten_3 (Flatten)             (None, 32)           0           mlp_item_embedding[0][0]         
__________________________________________________________________________________________________
mf_user_embedding (Embedding)   (None, 4)            3772        user_id[0][0]                    
__________________________________________________________________________________________________
mf_item_embedding (Embedding)   (None, 4)            6728        item_id[0][0]                    
__________________________________________________________________________________________________
concatenate (Concatenate)       (None, 64)           0           flatten_2[0][0]                  
                                                                 flatten_3[0][0]                  
__________________________________________________________________________________________________
flatten (Flatten)               (None, 4)            0           mf_user_embedding[0][0]          
__________________________________________________________________________________________________
flatten_1 (Flatten)             (None, 4)            0           mf_item_embedding[0][0]          
__________________________________________________________________________________________________
layer0 (Dense)                  (None, 8)            520         concatenate[0][0]                
__________________________________________________________________________________________________
multiply (Multiply)             (None, 4)            0           flatten[0][0]                    
                                                                 flatten_1[0][0]                  
__________________________________________________________________________________________________
layer1 (Dense)                  (None, 4)            36          layer0[0][0]                     
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 8)            0           multiply[0][0]                   
                                                                 layer1[0][0]                     
__________________________________________________________________________________________________
interaction (Dense)             (None, 1)            9           concatenate_1[0][0]              
==================================================================================================
Total params: 95,065
Trainable params: 95,065
Non-trainable params: 0
__________________________________________________________________________________________________
def make_tf_dataset(
    df: pd.DataFrame,
    targets: List[str],
    val_split: float = 0.1,
    batch_size: int = 512,
    seed=42,
):
    """Make TensorFlow dataset from Pandas DataFrame.
    :param df: input DataFrame - only contains features and target(s)
    :param targets: list of columns names corresponding to targets
    :param val_split: fraction of the data that should be used for validation
    :param batch_size: batch size for training
    :param seed: random seed for shuffling data - `None` won't shuffle the data"""

    n_val = round(df.shape[0] * val_split)
    if seed:
        # shuffle all the rows
        x = df.sample(frac=1, random_state=seed).to_dict("series")
    else:
        x = df.to_dict("series")
    y = dict()
    for t in targets:
        y[t] = x.pop(t)
    ds = tf.data.Dataset.from_tensor_slices((x, y))

    ds_val = ds.take(n_val).batch(batch_size)
    ds_train = ds.skip(n_val).batch(batch_size)
    return ds_train, ds_val
# create train and validation datasets
ds_train, ds_val = make_tf_dataset(df_train, ["interaction"])
%%time
# define logs and callbacks
logdir = os.path.join("logs", datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=0
)

train_hist = ncf_model.fit(
    ds_train,
    validation_data=ds_val,
    epochs=N_EPOCHS,
    callbacks=[tensorboard_callback, early_stopping_callback],
    verbose=1,
)
Epoch 1/10
/usr/local/anaconda3/envs/inteligencia-superficial/lib/python3.7/site-packages/tensorflow/python/framework/indexed_slices.py:432: UserWarning: Converting sparse IndexedSlices to a dense Tensor of unknown shape. This may consume a large amount of memory.
  "Converting sparse IndexedSlices to a dense Tensor of unknown shape. "
   1/2789 [..............................] - ETA: 0s - loss: 2.7761 - tp: 1.0000 - fp: 32.0000 - tn: 459.0000 - fn: 20.0000 - accuracy: 0.8984 - precision: 0.0303 - recall: 0.0476 - auc: 0.4325WARNING:tensorflow:From /usr/local/anaconda3/envs/inteligencia-superficial/lib/python3.7/site-packages/tensorflow/python/ops/summary_ops_v2.py:1277: stop (from tensorflow.python.eager.profiler) is deprecated and will be removed after 2020-07-01.
Instructions for updating:
use `tf.profiler.experimental.stop` instead.
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0031s vs `on_train_batch_end` time: 0.0254s). Check your callbacks.
2789/2789 [==============================] - 9s 3ms/step - loss: 0.2318 - tp: 1691.0000 - fp: 789.0000 - tn: 1359625.0000 - fn: 65408.0000 - accuracy: 0.9536 - precision: 0.6819 - recall: 0.0252 - auc: 0.8033 - val_loss: 0.1408 - val_tp: 902.0000 - val_fp: 416.0000 - val_tn: 150669.0000 - val_fn: 6626.0000 - val_accuracy: 0.9556 - val_precision: 0.6844 - val_recall: 0.1198 - val_auc: 0.9020
Epoch 2/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1279 - tp: 11668.0000 - fp: 6340.0000 - tn: 1354074.0000 - fn: 55431.0000 - accuracy: 0.9567 - precision: 0.6479 - recall: 0.1739 - auc: 0.9164 - val_loss: 0.1236 - val_tp: 1532.0000 - val_fp: 854.0000 - val_tn: 150231.0000 - val_fn: 5996.0000 - val_accuracy: 0.9568 - val_precision: 0.6421 - val_recall: 0.2035 - val_auc: 0.9195
Epoch 3/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1191 - tp: 13715.0000 - fp: 7758.0000 - tn: 1352656.0000 - fn: 53384.0000 - accuracy: 0.9572 - precision: 0.6387 - recall: 0.2044 - auc: 0.9254 - val_loss: 0.1198 - val_tp: 1587.0000 - val_fp: 835.0000 - val_tn: 150250.0000 - val_fn: 5941.0000 - val_accuracy: 0.9573 - val_precision: 0.6552 - val_recall: 0.2108 - val_auc: 0.9232
Epoch 4/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1148 - tp: 14333.0000 - fp: 7576.0000 - tn: 1352838.0000 - fn: 52766.0000 - accuracy: 0.9577 - precision: 0.6542 - recall: 0.2136 - auc: 0.9293 - val_loss: 0.1160 - val_tp: 1610.0000 - val_fp: 797.0000 - val_tn: 150288.0000 - val_fn: 5918.0000 - val_accuracy: 0.9577 - val_precision: 0.6689 - val_recall: 0.2139 - val_auc: 0.9267
Epoch 5/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1114 - tp: 15531.0000 - fp: 7649.0000 - tn: 1352765.0000 - fn: 51568.0000 - accuracy: 0.9585 - precision: 0.6700 - recall: 0.2315 - auc: 0.9335 - val_loss: 0.1138 - val_tp: 1777.0000 - val_fp: 877.0000 - val_tn: 150208.0000 - val_fn: 5751.0000 - val_accuracy: 0.9582 - val_precision: 0.6696 - val_recall: 0.2361 - val_auc: 0.9294
Epoch 6/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1088 - tp: 16978.0000 - fp: 8344.0000 - tn: 1352070.0000 - fn: 50121.0000 - accuracy: 0.9590 - precision: 0.6705 - recall: 0.2530 - auc: 0.9373 - val_loss: 0.1120 - val_tp: 1927.0000 - val_fp: 975.0000 - val_tn: 150110.0000 - val_fn: 5601.0000 - val_accuracy: 0.9585 - val_precision: 0.6640 - val_recall: 0.2560 - val_auc: 0.9317
Epoch 7/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1069 - tp: 18235.0000 - fp: 9057.0000 - tn: 1351357.0000 - fn: 48864.0000 - accuracy: 0.9594 - precision: 0.6681 - recall: 0.2718 - auc: 0.9401 - val_loss: 0.1108 - val_tp: 2033.0000 - val_fp: 1031.0000 - val_tn: 150054.0000 - val_fn: 5495.0000 - val_accuracy: 0.9589 - val_precision: 0.6635 - val_recall: 0.2701 - val_auc: 0.9338
Epoch 8/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1055 - tp: 19127.0000 - fp: 9621.0000 - tn: 1350793.0000 - fn: 47972.0000 - accuracy: 0.9597 - precision: 0.6653 - recall: 0.2851 - auc: 0.9421 - val_loss: 0.1100 - val_tp: 2113.0000 - val_fp: 1069.0000 - val_tn: 150016.0000 - val_fn: 5415.0000 - val_accuracy: 0.9591 - val_precision: 0.6640 - val_recall: 0.2807 - val_auc: 0.9350
Epoch 9/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1046 - tp: 19749.0000 - fp: 9984.0000 - tn: 1350430.0000 - fn: 47350.0000 - accuracy: 0.9598 - precision: 0.6642 - recall: 0.2943 - auc: 0.9435 - val_loss: 0.1094 - val_tp: 2154.0000 - val_fp: 1107.0000 - val_tn: 149978.0000 - val_fn: 5374.0000 - val_accuracy: 0.9591 - val_precision: 0.6605 - val_recall: 0.2861 - val_auc: 0.9357
Epoch 10/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1040 - tp: 20082.0000 - fp: 10168.0000 - tn: 1350246.0000 - fn: 47017.0000 - accuracy: 0.9599 - precision: 0.6639 - recall: 0.2993 - auc: 0.9445 - val_loss: 0.1090 - val_tp: 2191.0000 - val_fp: 1126.0000 - val_tn: 149959.0000 - val_fn: 5337.0000 - val_accuracy: 0.9593 - val_precision: 0.6605 - val_recall: 0.2910 - val_auc: 0.9364
CPU times: user 2min 29s, sys: 43.4 s, total: 3min 12s
Wall time: 1min 20s
long_test = wide_to_long(data["train"], unique_ratings)
df_test = pd.DataFrame(long_test, columns=["user_id", "item_id", "interaction"])
ds_test, _ = make_tf_dataset(df_test, ["interaction"], val_split=0, seed=None)
%%time
ncf_predictions = ncf_model.predict(ds_test)
df_test["ncf_predictions"] = ncf_predictions
CPU times: user 3.81 s, sys: 210 ms, total: 4.02 s
Wall time: 3.69 s
user_id item_id interaction ncf_predictions
0 0 7 0 0.523643
1 0 10 0 0.719504
2 0 19 0 0.100669
3 0 20 0 0.123813
4 0 26 0 0.102480

# collapse
data["ncf_predictions"] = df_test.pivot(
    index="user_id", columns="item_id", values="ncf_predictions"
).values
print("Neural collaborative filtering predictions")
print(data["ncf_predictions"][:10, :4])
Neural collaborative filtering predictions
[[7.7809501e-01 3.4897393e-01 2.3736593e-01 7.5093412e-01]
 [1.5352371e-01 1.8476248e-03 2.3163706e-02 3.6399364e-03]
 [4.6624422e-02 4.7096610e-04 1.2840241e-02 1.1576419e-04]
 [8.5962385e-02 1.4925003e-03 6.1967373e-03 5.1632524e-04]
 [5.8516884e-01 2.8336483e-01 7.5634271e-02 3.0715367e-01]
 [4.0988737e-01 2.2669524e-02 1.0599941e-02 4.0282601e-01]
 [6.0177052e-01 6.6075641e-01 7.8367621e-02 8.1673837e-01]
 [4.9012059e-01 8.9323461e-02 6.3689947e-03 6.7939401e-02]
 [1.5069479e-01 1.3713539e-03 2.8979778e-04 2.2239387e-03]
 [5.0181168e-01 6.9155514e-02 3.4887791e-02 4.8452517e-01]]
precision_ncf = tf.keras.metrics.Precision(top_k=TOP_K)
recall_ncf = tf.keras.metrics.Recall(top_k=TOP_K)

precision_ncf.update_state(data["test"], data["ncf_predictions"])
recall_ncf.update_state(data["test"], data["ncf_predictions"])
print(
    f"At K = {TOP_K}, we have a precision of {precision_ncf.result().numpy():.5f}",
    "and a recall of {recall_ncf.result().numpy():.5f}",
)
At K = 5, we have a precision of 0.10859 and a recall of 0.06487
%%time
# LightFM model
def norm(x: float) -> float:
    """Normalize vector"""
    return (x - np.min(x)) / np.ptp(x)


lightfm_model = LightFM(loss="warp")
lightfm_model.fit(sparse.coo_matrix(data["train"]), epochs=N_EPOCHS)

lightfm_predictions = lightfm_model.predict(
    df_test["user_id"].values, df_test["item_id"].values
)
df_test["lightfm_predictions"] = lightfm_predictions
wide_predictions = df_test.pivot(
    index="user_id", columns="item_id", values="lightfm_predictions"
).values
data["lightfm_predictions"] = norm(wide_predictions)

# compute the metrics
precision_lightfm = tf.keras.metrics.Precision(top_k=TOP_K)
recall_lightfm = tf.keras.metrics.Recall(top_k=TOP_K)
precision_lightfm.update_state(data["test"], data["lightfm_predictions"])
recall_lightfm.update_state(data["test"], data["lightfm_predictions"])
print(
    f"At K = {TOP_K}, we have a precision of {precision_lightfm.result().numpy():.5f}",
    "and a recall of {recall_lightfm.result().numpy():.5f}",
)
At K = 5, we have a precision of 0.10541 and a recall of 0.06297
CPU times: user 1.01 s, sys: 235 ms, total: 1.25 s
Wall time: 858 ms